#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import os
import re
import time

from common.common import check_path_legal, is_clone_file_system
from common.common_models import SubJobModel
from common.const import SubJobTypeEnum, SubJobPolicyEnum, ExecuteResultEnum, CMDResult
from common.file_common import get_user_info
from common.util.cmd_utils import get_livemount_path
from generaldb.gbase8a import LOGGER
from generaldb.gbase8a.commands.gbase_cmd import GbaseCmd
from generaldb.gbase8a.common.common_models import RestoreCmdParam, ModifyPermission
from generaldb.gbase8a.common.const import (
    SUPPORT_VERSION, GbasePath, ErrorCode, NormalErr, RestoreSubJobName, PexpectResult, LinkTaskType)
from generaldb.gbase8a.common.copy_link import CopyLink
from generaldb.gbase8a.common.file_operate import FileOperate
from generaldb.gbase8a.common.gbase_common import look_up_gcrcman_process, remove_file_dir_link
from generaldb.gbase8a.param.restore_param_parser import RestoreParam
from generaldb.gbase8a.service.base_service import MetaServiceWorker
from generaldb.gbase8a.service.resource.resource_service import GBaseInfos
from generaldb.gbase8a.util.gbase_util import GbaseUtil
from common.common import execute_cmd


class RestoreManager(MetaServiceWorker):

    def __init__(self, job_manager, param: RestoreParam):
        """
        根据调用进程id和入参脚本对象生成具体执行恢复的对象，实现恢复过程的依赖功能
        :param pid:
        :param param:
        """
        super().__init__(job_manager, param)
        self.cmd = GbaseCmd(self.job_manager.pid)
        self.vc_name = param.get_target_vcname()
        self.db_name = param.get_target_db_name()
        self.default_msg = ""
        self.data_path_link = os.path.join(GbasePath.GBASE_LINK_PATH, self.param.job_id)

    def check_db_os_user(self):
        """检查是否存在数据库操作系统用户"""
        os_user = self.cmd.get_os_user()
        _, user_info = get_user_info(os_user)
        if not user_info:
            msg = "Get user info failed"
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.ERROR_USER_NOT_EXIST_CLUSTER, msg=msg)
        LOGGER.info("Get db os user info success.")
        return self.update_action_result(NormalErr.NO_ERR)

    def allow_execute_location(self):
        """恢复任务只能在coordinator且gcware角色节点执行"""
        if not self.is_manager_role():
            msg = "Not allow restore local,only coordinator node."
            return self.update_action_result(code=NormalErr.FALSE.value, body_err_code=ErrorCode.ERR_RESTORED, msg=msg)
        LOGGER.info("Allow execute job on local node.")
        return self.can_restore()

    def show_databases(self):
        """列举数据库，可用于数据库用户名与密码验证"""
        expect_envs = [(self.cmd.get_db_pwd_key(), PexpectResult.DB_LOGIN)]
        os_user = self.cmd.get_os_user()
        db_user = self.cmd.get_db_user()
        ret, database = self.cmd.show_databases(os_user, db_user, expect_envs, self.vc_name)
        if not ret:
            msg = "Show database failed, check the auth "
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.ERROR_AUTH, msg=msg)
        LOGGER.info("Show database successful.")
        return self.update_action_result(NormalErr.NO_ERR)

    def check_cluster_mode(self):
        if not self.is_manager_role():
            msg = "Not manager role node, report default success."
            LOGGER.info(msg)
            return self.update_action_result(NormalErr.NO_ERR, msg=msg)
        os_user = self.cmd.get_os_user()
        ret, mode = self.cmd.show_gcluster_mode(os_user)
        if not ret:
            msg = "Check mode failed"
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.OPERATE_FAILED, msg=msg)
        cluster_mode = re.search(r"VIRTUAL CLUSTER MODE:\s+(.*)", mode)
        if cluster_mode and cluster_mode.group(1).strip() in ("RESTORE", "NORMAL"):
            msg = "Check cluster mode success."
            LOGGER.info(msg)
            return self.update_action_result(NormalErr.NO_ERR, msg=msg)
        msg = "Cluster status not support restore"
        LOGGER.error(msg)
        return self.update_action_result(ErrorCode.ERR_DATABASE_STATUS, msg=msg)

    def check_gcrcman_process(self):
        """检查是否有gcrcman进程正在运行中"""
        os_user = self.cmd.get_os_user()
        gcrcman_path = GbaseUtil.get_gcrcman_path(self.cmd, os_user)
        if look_up_gcrcman_process(gcrcman_path):
            msg = "Exist gcrcman process, need wait it for completed."
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.PROCESS_EXISTED, msg=msg)
        LOGGER.info("No gcrcman is running")
        return self.update_action_result(NormalErr.NO_ERR)

    def check_db_version(self):
        """检查环境数据库版本并校验副本版本的一致性"""
        os_user = self.cmd.get_os_user()
        ret, version = self.cmd.get_db_version(os_user)
        if not ret:
            msg = "Get db version with execute cmd failed"
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.OPERATE_FAILED, msg=msg)
        if SUPPORT_VERSION not in version:
            msg = "Db version not supported ."
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.ERROR_DIFFERENT_VERSION, msg=msg)
        src_db_version = self.param.get_src_db_version()
        if src_db_version not in version:
            msg = "Target cluster db version is different from the origin."
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.ERROR_DIFFERENT_VERSION, msg=msg)
        LOGGER.info("Check db version successful")
        return self.update_action_result(NormalErr.NO_ERR)

    def create_datapath_symlink(self):
        """涉及创建连接为低权限用户，不通过标准库创建"""
        data_path = self.param.get_data_path()
        if not data_path:
            msg = "Not find the data path."
            LOGGER.error(msg)
            return self.update_action_result(code=NormalErr.FALSE.value, body_err_code=ErrorCode.OPERATE_FAILED,
                                             msg=msg)
        os_user = self.cmd.get_os_user()
        if not os.path.exists(GbasePath.GBASE_LINK_PATH) \
                and not self.cmd.mk_user_dir(GbasePath.GBASE_LINK_PATH, os_user):
            msg = "Create link path failed."
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.OPERATE_FAILED, msg=msg)
        # 组织恢复目录
        mount_path = get_livemount_path(self.param.job_id, data_path[0])
        LOGGER.info(f"Get mount path: {mount_path}.")
        ret = self.get_mount_path(mount_path)
        if isinstance(ret, ErrorCode):
            return self.update_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value, body_err_code=ret)
        if not os.path.exists(ret):
            msg = f"Not exists the mount path.path:{ret}"
            LOGGER.error(msg)
            return self.update_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                             body_err_code=ErrorCode.OPERATE_FAILED,
                                             msg=msg)
        # 创建恢复目录软链接
        sub_paths = os.listdir(mount_path)
        # .snapshot 目录不能修改权限
        if ".snapshot" in sub_paths:
            sub_paths.remove(".snapshot")
        for sub_path in sub_paths:
            path = os.path.join(mount_path, sub_path)
            return_code, out, err = execute_cmd(f"chown -R {os_user}:{os_user} {path}")
            if return_code != CMDResult.SUCCESS:
                return self.update_action_result(code=NormalErr.FALSE.value, body_err_code=ErrorCode.OPERATE_FAILED)
        return self.create_link(mount_path, os_user, ret)

    def get_mount_path(self, data_path):
        copy_id = self.param.get_recover_copy_id()
        agent_id = GbaseUtil.get_agent_id(self.param)
        if isinstance(agent_id, ErrorCode):
            return agent_id
        return os.path.join(data_path, copy_id, agent_id)

    def mkdir(self):
        host_name = self.cmd.get_os_user()
        path_info = ModifyPermission(path=GbasePath.GBASE_LINK_PATH, user=host_name, path_check_func=check_path_legal,
                                     path_check_func_args=(GbasePath.GBASE_LINK_PATH,
                                                           GbasePath.GBASE_FILESYSTEM_MOUNT_PATH))
        if os.path.exists(GbasePath.GBASE_LINK_PATH):
            if not FileOperate.change_path_permission(path_info):
                return False
        elif not FileOperate.mkdir_change_permission(path_info):
            LOGGER.error("Create local link path fail")
            return False
        return True

    def create_link(self, data_path, os_user, link_path):
        self.mkdir()
        copy_link = CopyLink(self.param.get_copy_id_list(), data_path, os_user, LinkTaskType.RESTORE, self.param)
        if not copy_link.main():
            LOGGER.error("Failed to build restore directory")
            return self.update_action_result(code=NormalErr.FALSE.value, body_err_code=ErrorCode.OPERATE_FAILED)
        if os.path.exists(self.data_path_link):
            remove_file_dir_link(self.data_path_link)
        ret = self.cmd.create_datapath_symlink(os_user, link_path, self.data_path_link)
        if not ret:
            return self.update_action_result(code=NormalErr.FALSE.value, body_err_code=ErrorCode.OPERATE_FAILED)
        if not os.path.exists(self.data_path_link):
            LOGGER.error("Create link fail, path not exist:%s", self.data_path_link)
            return self.update_action_result(code=NormalErr.FALSE.value, body_err_code=ErrorCode.OPERATE_FAILED)
        LOGGER.info("Create link success.link path:%s", self.data_path_link)
        # 修改目录权限
        if is_clone_file_system(self.param.get_param_dict()):
            sub_paths = os.listdir(data_path)
            # .snapshot 目录不能修改权限
            if ".snapshot" in sub_paths:
                sub_paths.remove(".snapshot")
            for sub_path in sub_paths:
                path = os.path.join(data_path, sub_path)
                FileOperate.recursive_modification_permission(path, GbasePath.GBASE_FILESYSTEM_MOUNT_PATH,
                                                              os_user, mode='ug+w')
        return self.update_action_result(NormalErr.NO_ERR)

    def can_restore(self):
        ret, err = self.cmd.check_os_user()
        if not ret:
            LOGGER.error(f"auth failed, error:%s", err)
            return self.update_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                             body_err_code=ErrorCode.ERROR_AUTH,
                                             msg="auth failed")
        # 不允许恢复到不同名称的数据库
        copy_db = self.param.get_recover_db_name()
        target_db = self.param.get_target_db_name()
        if copy_db != target_db:
            error_msg = f"Not allow restore to a different database, copy:{copy_db}, target:{target_db}"
            LOGGER.error(error_msg)
            return self.update_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                             body_err_code=ErrorCode.ERROR_DIFFERENT_DB,
                                             msg=error_msg, err_param=[copy_db, target_db])
        if not GbaseUtil.has_pexpect(self.cmd.get_os_user()):
            error = "No module named 'pexpect'"
            LOGGER.error(error)
            return self.update_action_result(code=ExecuteResultEnum.INTERNAL_ERROR.value,
                                             body_err_code=ErrorCode.ERROR_NO_PEXPECT,
                                             msg=error)
        return self.update_action_result(NormalErr.NO_ERR)

    def check_copies(self):
        envs = [
            (GbaseCmd.get_os_pwd_key(), PexpectResult.OS_LOGIN),
            (self.cmd.get_db_pwd_key(), PexpectResult.DB_LOGIN)
        ]
        os_user = self.cmd.get_os_user()
        gcrcmman_path = GbaseUtil.get_gcrcman_path(self.cmd, os_user)
        ret, copies = self.cmd.show_backup(gcrcmman_path, os_user, self.data_path_link, envs)
        if not ret:
            msg = f"Check the copies failed with get backup info err:{copies} "
            if "No module named pexpect" in copies:
                return self.update_action_result(ErrorCode.ERROR_NO_PEXPECT, msg=msg)
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.OPERATE_FAILED, msg=msg)

        LOGGER.info("Check copies successful.")
        return self.update_action_result(NormalErr.NO_ERR)

    def drop_database(self):
        """删除数据库"""
        if not self.db_name:
            self.db_name = self.param.get_recover_db_name()
        expect_envs = [(self.cmd.get_db_pwd_key(), PexpectResult.DB_LOGIN)]
        os_user = self.cmd.get_os_user()
        db_user = self.cmd.get_db_user()
        ret, cont = self.cmd.drop_database(
            os_user, db_user,
            db_name=self.db_name,
            expect_envs=expect_envs)
        if not ret and "database doesn't exist" not in cont:
            msg = "Drop database failed.with error:%s" % cont
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.OPERATE_FAILED, msg=msg)
        LOGGER.info("Drop database successful.")
        return self.update_action_result(NormalErr.NO_ERR)

    def execute_restore(self):
        """执行恢复子任务"""
        if not self.db_name:
            self.db_name = self.param.get_recover_db_name()
        cycle_id, point_id = self.param.get_recover_cycle_point()
        LOGGER.info("Starting recover db:%s", self.db_name)
        if not point_id:
            msg = "Recover database failed. with point param err"
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.ERR_RESTORED, msg=msg)
        os_user = self.cmd.get_os_user()
        gcrcman_path = GbaseUtil.get_gcrcman_path(self.cmd, os_user)
        expect_envs = [
            (GbaseCmd.get_os_pwd_key(), PexpectResult.OS_LOGIN),
            (self.cmd.get_db_pwd_key(), PexpectResult.DB_LOGIN)
        ]
        param = RestoreCmdParam(gcrcman=gcrcman_path,
                                db_os_user=os_user,
                                copy_dir=self.data_path_link,
                                db_name=self.db_name,
                                cycle_id=cycle_id, point_id=point_id)
        ret, cont = self.cmd.restore_database(param, env_keys=expect_envs)
        if not ret:
            msg = "Recover database failed.with error:%s" % cont
            LOGGER.error(msg)
            if "error:the topology of cluster have been changed" in cont:
                return self.update_action_result(ErrorCode.ERROR_TOPOLOGY, msg=msg)
            return self.update_action_result(ErrorCode.ERROR_EXECUTE_RESTORE_CMD, msg=msg)
        LOGGER.info("Recover database successful.")
        return self.update_action_result(NormalErr.NO_ERR)

    def refresh_db_tables(self):
        """恢复完成后刷新数据库"""
        expect_envs = [(self.cmd.get_db_pwd_key(), PexpectResult.DB_LOGIN)]
        os_user = self.cmd.get_os_user()
        db_user = self.cmd.get_db_user()
        ret, cont = self.cmd.refresh_database(
            os_user, db_user,
            expect_envs=expect_envs, )
        if not ret:
            msg = "Refresh database failed.with error:%s" % cont
            LOGGER.error(msg)
            return self.update_action_result(ErrorCode.ERR_RESTORED, msg=msg)
        LOGGER.info("Refresh database successful.")
        return self.update_action_result(NormalErr.NO_ERR)

    def delete_datapath_symlink(self):
        """切用户删除创建的连接"""

        # 只检验软连接文件是否存在
        if not os.path.lexists(self.data_path_link):
            return self.update_action_result(NormalErr.NO_ERR)
        retry_times = 3
        os_user = self.cmd.get_os_user()
        for _ in range(retry_times):
            if os.path.lexists(self.data_path_link):
                self.cmd.delete_datapath_symlink(os_user, self.data_path_link)
            else:
                LOGGER.info("Delete symlink path successful.")
                return self.update_action_result(NormalErr.NO_ERR)
        msg = "Delete link path failed."
        LOGGER.error(msg)
        return self.update_action_result(ErrorCode.OPERATE_FAILED, msg=msg)

    def clear_cache_data(self):
        pass

    def is_coordinator_role(self):
        os_user = self.cmd.get_os_user()
        coord_name = GBaseInfos.get_local_coordinator_name(os_user, self.cmd)

        return coord_name != ""

    def is_gcware_role(self):
        os_user = self.cmd.get_os_user()
        gcware_name = GBaseInfos.get_local_gcware_name(os_user, self.cmd)

        return gcware_name != ""

    def is_manager_role(self):
        return self.is_coordinator_role() and self.is_gcware_role()

    def change_mount(self):
        pass

    def gen_sub_jobs(self, job_id):
        """拆分恢复子任务"""
        all_nodes_info = self.param.parse_target_env_nodes()
        all_nodes_status = self.param.parse_target_env_extend_nodes()
        sub_jobs = []
        # 是否选择了coordinator 节点
        flag = False

        def _gen_restore_sub_jobs(n_id, exc_jobs):
            gen_jobs = []
            for ind, job_name in enumerate(exc_jobs):
                job_priority = ind + 1
                gen_jobs.append(
                    SubJobModel(
                        jobId=job_id,
                        subJobId="",
                        jobType=SubJobTypeEnum.BUSINESS_SUB_JOB.value,
                        jobName=job_name,
                        jobPriority=job_priority, policy=SubJobPolicyEnum.FIXED_NODE.value, ignoreFailed=False,
                        execNodeId=n_id, jobInfo="").dict(by_alias=True)
                )
            return gen_jobs

        for node_ip, node_info in all_nodes_status.items():
            node_id = all_nodes_info.get(node_ip)
            if node_info.get("coordinator_name") and not flag:
                jobs = (RestoreSubJobName.PREPARE_RESTORE, RestoreSubJobName.RESTORE)
                flag = True
            else:
                jobs = (RestoreSubJobName.PREPARE_RESTORE,)
            restore_sub_jobs = _gen_restore_sub_jobs(node_id, jobs)
            sub_jobs.extend(restore_sub_jobs)
        self.job_manager.update(sub_jobs)
